
Muchos de los servicios principales de MELI son públicos, lo cual permite acceder a los datos de forma sencilla. En la primera parte del desafío el objetivo es realizar un análisis exploratorio de las publicaciones con descuento del marketplace. Las preguntas a responder y el enfoque del análisis son libres. Como punto de partida se puede sugiere utilizar la API de search de mercadolibre, la cual está por detrás del buscador de Mercadolibre.
En esta parte del desafío, las preguntas son abiertas, pero a modo de ayuda estas son algunas de las que se podrían responder:
1.1. Categorías y subcategorías
1.2. Generación del dataset
1.3. Análisis
1.4. Mejoras en el código y siguientes pasos
Arrancamos revisando cómo están distribuidos los servicios de MercadoLibre. En particular, vale mencionar los identificadores de cada uno de los sites o portales de MercadoLibre para cada país. Así, el identificador de Argentina es MLA, el de Chile es MLC y el de Colombia es MCO [ver listado completo]. El presente desarrollo está basado en el site de MercadoLibre para Colombia, con código MCO.
Enseguida, notamos que el sitio cuenta con categorías ("Carros, Motos y Otros" , "Computación" , "Celulares y Teléfonos",...) y subcategorías para cada una de las primeras (para la categoría "Celulares y Teléfonos" hay subcategorías como "Accesorios para Celulares", "Celulares y Smartphones", "Repuestos de Celulares",...). Cada categoría y subcategoría cuenta con un identificador único que inicia con los caracteres "MCO".
Entonces, lo primero que haremos será elegir algunas subcategorías, traernos los datos aprovechando los atributos en su API, armar un dataframe y trabajar en el análisis:
# Para cargar las librerías que usaremos
import pandas as pd # Para el manejo de dataframes
import numpy as np # Para algunas operaciones numéricas
import requests # Para las solicitudes a la API
import json # Para trabajar con formatos JSON
from pandas.io.json import json_normalize # Para aplanar los JSON y convertirlos en DataFrame
import plotly.express as px # Para gráficos sencillos pero elegantes e interactivos
import pandas_profiling as pf # Para facilitar el análisis descriptivo y exploratorio
# Para traer todo el listado de categorías:
file_json = requests.get("https://api.mercadolibre.com/sites/MCO/categories").json() # Para hacer la solicitud "GET" a la API y tomar la parte de los datos en formato JSON
categories = pd.DataFrame(file_json) # Para convertir en dataframe los datos traídos
display(categories[10:20]) # Para desplegar el listado de categorías
| id | name | |
|---|---|---|
| 10 | MCO1743 | Carros, Motos y Otros |
| 11 | MCO1051 | Celulares y Teléfonos |
| 12 | MCO1648 | Computación |
| 13 | MCO1144 | Consolas y Videojuegos |
| 14 | MCO1276 | Deportes y Fitness |
| 15 | MCO5726 | Electrodomésticos |
| 16 | MCO1000 | Electrónica, Audio y Video |
| 17 | MCO175794 | Herramientas y Construcción |
| 18 | MCO1574 | Hogar y Muebles |
| 19 | MCO1499 | Industrias y Oficinas |
Nos iremos con la categoría "Celulares y Teléfonos". Ahora, buscaremos una subcategoría interesante.
def subcategories(MCO): # Función para mostrar una tabla con las subcategorías a partir del MCO
subcategories_df = requests.get("https://api.mercadolibre.com/categories/" + str(MCO)).json()
subcategories_df = pd.DataFrame(subcategories_df["children_categories"]) # Dentro del JSON, children_categories es la key asociada a las subcategorías
return(subcategories_df)
subcategories("MCO1051")
| id | name | total_items_in_this_category | |
|---|---|---|---|
| 0 | MCO3813 | Accesorios para Celulares | 823458 |
| 1 | MCO1055 | Celulares y Smartphones | 38458 |
| 2 | MCO401278 | Gafas de Realidad Virtual | 952 |
| 3 | MCO1058 | Radios y Handies | 20145 |
| 4 | MCO442202 | Repuestos de Celulares | 72800 |
| 5 | MCO417704 | Smartwatches y Accesorios | 58012 |
| 6 | MCO10616 | Tarificadores y Cabinas | 1113 |
| 7 | MCO1053 | Telefonía Fija e Inalámbrica | 6538 |
| 8 | MCO5237 | Telefonía IP | 2964 |
| 9 | MCO1915 | Otros | 9564 |
Considerando la razonable cantidad de ítems y su relevancia para los compradores en línea, aquí trabajaremos con "Celulares y Smartphones". Su identificador es MCO1055.
Ahora repetimos el proceso para otra subcategoría interesante pero ahora en la categoría "Electrónica, Audio y Video":
subcategories("MCO1000")
| id | name | total_items_in_this_category | |
|---|---|---|---|
| 0 | MCO3690 | Accesorios Audio y Video | 79158 |
| 1 | MCO431414 | Accesorios para TV | 64726 |
| 2 | MCO3835 | Audio | 440692 |
| 3 | MCO5054 | Cables | 206029 |
| 4 | MCO11830 | Componentes Electrónicos | 184268 |
| 5 | MCO4632 | Controles Remotos | 55006 |
| 6 | MCO176837 | Convertidores a Smart TV | 6647 |
| 7 | MCO173235 | Drones y Accesorios | 67677 |
| 8 | MCO442042 | Fundas y Bolsos | 1506 |
| 9 | MCO4102 | Pilas y Cargadores | 43341 |
| 10 | MCO419930 | Repuestos TV | 16729 |
| 11 | MCO14903 | Televisores | 12436 |
| 12 | MCO442056 | Video | 16584 |
| 13 | MCO4800 | Video Beams y Pantallas | 38470 |
| 14 | MCO1070 | Otros | 48329 |
"Televisores" parece interesante por lo que trabajaremos con ella siendo su código el MCO14903. Complementaremos con "Video Beams y Pantallas", "Accesorios Audio y Video" y "Smartwatches y Accesorios" (MCO4800, MCO3690 y MCO417704, respectivamente): son de temática similar y se han hecho más necesarios que nunca con el tema de la pandemia.
En resumen, fueron 5 los elegidos:
Ya con los códigos elegidos, resta armar el dataset inicial yendo a consultar a la API de MercadoLibre directamente con dichos códigos:
selected_codes = {"MCO1055": "cellphones", "MCO417704":"smartwatches", "MCO14903": "televisions", "MCO4800": "screens", "MCO3690":"audio"}
¿Cuántos registros hay por cada código?
for code in selected_codes.keys():
registros = requests.get("https://api.mercadolibre.com/sites/MCO/search?category="+ code).json()['paging']['total'] # Número total de registros
print(code, selected_codes[code], str("-> " + str(registros) + " registros"))
MCO1055 cellphones -> 37158 registros MCO417704 smartwatches -> 69984 registros MCO14903 televisions -> 11730 registros MCO4800 screens -> 38368 registros MCO3690 audio -> 75638 registros
limit y offset proveídos por la API [Más información]
n = 1000 # registros por categoría
import requests
from requests_oauthlib import OAuth1
auth = OAuth1('7393870389611734',
'7pFsN44Pif4o8J1PZnQ4m1dsZkQMF86Z',
'localhost',
'MCO'
)
final_base = pd.DataFrame()
for code in selected_codes.keys():
base = pd.DataFrame()
for i in range(0, n, 50):
items = requests.get("https://api.mercadolibre.com/sites/MCO/search?category="+ code + "&offset=" + str(i) +
"&limit=50", auth=auth).json() # Para armar las URL
items = pd.json_normalize(items['results']) # Para "aplanar" los JSON
base = base.append(items, ignore_index=True) # Reiniciar índices
base['sub_cat'] = selected_codes[code] # Variable de categorías
final_base = final_base.append(base, ignore_index=True)
base.to_csv("data/"+ selected_codes[code] + ".csv", sep='|', encoding = "utf-8") #Exportar resultados por si no se desea llamar la API cada vez
Para traer más de 1.000 registros, hay otra forma de realizar la búsqueda. Queda pendiente su implementación para futuros trabajos.
Y así ya tenemos la base final, con 5.000 registros de nuestros interés, 1.000 por cada una de las categorías elegidas:
print(final_base.shape)
final_base.sample(5)
(5000, 58)
| id | site_id | title | price | sale_price | currency_id | available_quantity | sold_quantity | buying_mode | listing_type_id | ... | seller_address.state.id | seller_address.state.name | seller_address.city.id | seller_address.city.name | seller_address.latitude | seller_address.longitude | sub_cat | differential_pricing.id | installments | prices | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3678 | MCO552244865 | MCO | Proyector 3d 3500 Lumens Dlp Viewsonic Pjd6544... | 1350000 | None | COP | 1 | 0 | buy_it_now | gold_pro | ... | CO-DC | Bogotá D.C. | TUNPQ1BVRTg0NTU0 | Puente Aranda | screens | 33602181.0 | NaN | NaN | ||
| 2422 | MCO582101026 | MCO | Caja De Tv De Pantalla Plana Ecobox De 32 A 37... | 515990 | None | COP | 1 | 0 | buy_it_now | gold_pro | ... | CO-SAN | Santander | TUNPQ0JVQzkzMTE4 | Bucaramanga | televisions | 33602181.0 | NaN | NaN | ||
| 2238 | MCO587694042 | MCO | Televisor 4k Xiaomi De 55p Con Certificación N... | 2199999 | None | COP | 1 | 0 | buy_it_now | gold_special | ... | CO-SAN | Santander | TUNPQ0JVQzkzMTE4 | Bucaramanga | televisions | NaN | NaN | NaN | ||
| 3609 | MCO500355489 | MCO | Pantalla De Proyector De 60 Pulgadas Proyecció... | 162000 | None | COP | 1 | 0 | buy_it_now | gold_special | ... | CO-DC | Bogotá D.C. | TUNPQ0VORzkzMTUz | Engativá | screens | NaN | NaN | NaN | ||
| 3179 | MCO531718236 | MCO | Mini Proyector Led Hd Video Beam Hdmi Sd Uc28 ... | 228900 | None | COP | 1 | 25 | buy_it_now | gold_special | ... | CO-DC | Bogotá D.C. | TUNPQ1BVRTg0NTU0 | Puente Aranda | screens | NaN | NaN | NaN |
5 rows × 58 columns
Lo más importante es tener en cuenta el objetivo del análisis, en este caso, extraer conocimiento desde las publicaciones en diferentes categorías de productos. Una buena idea es realizar un análisis para conocer las distribuciones de cada variable, sus categorías y sus tipos así como revisar datos atípicos, datos faltantes y relaciones entre ellas.
final_base.dtypes
id object site_id object title object price int64 sale_price object currency_id object available_quantity int64 sold_quantity int64 buying_mode object listing_type_id object stop_time object condition object permalink object thumbnail object accepts_mercadopago bool attributes object original_price object category_id object official_store_id object domain_id object catalog_product_id object tags object order_backend int64 seller.id int64 seller.permalink object seller.registration_date object seller.car_dealer bool seller.real_estate_agency bool seller.tags object installments.quantity float64 installments.amount float64 installments.rate float64 installments.currency_id object address.state_id object address.state_name object address.city_id object address.city_name object shipping.free_shipping bool shipping.mode object shipping.tags object shipping.logistic_type object shipping.store_pick_up bool seller_address.id object seller_address.comment object seller_address.address_line object seller_address.zip_code object seller_address.country.id object seller_address.country.name object seller_address.state.id object seller_address.state.name object seller_address.city.id object seller_address.city.name object seller_address.latitude object seller_address.longitude object sub_cat object differential_pricing.id float64 installments float64 prices float64 dtype: object
# Para crear un reporte analizando todas las variables en conjunto
profile = pf.ProfileReport(final_base)
profile


Muchas de las correlaciones son triviales y se infieren por el nombre, por ejemplo, la correlación entre todas las variables de ubicación (address), sin embargo, a la hora de modelar, sí vale la pena echarle un vistazo más detallado a correlaciones entre numéricas, en especial, a todas las relacionadas con precios y ventas (incluyendo la correlación entre original_price y sold_quantity ).
# Estadísticas básicas sobre el precio por categoría
final_base.groupby("sub_cat")['price'].describe().reset_index()
| sub_cat | count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | audio | 1000.0 | 100153.097 | 2.475927e+05 | 1100.0 | 14900.0 | 34900.0 | 99225.0 | 4510990.0 |
| 1 | cellphones | 1000.0 | 1028475.712 | 1.220780e+06 | 2000.0 | 329900.0 | 699900.0 | 1099000.0 | 10299000.0 |
| 2 | screens | 1000.0 | 2295724.408 | 1.182522e+07 | 1000.0 | 230000.0 | 513449.5 | 1296447.0 | 216204000.0 |
| 3 | smartwatches | 1000.0 | 120711.808 | 2.916786e+05 | 1500.0 | 17900.0 | 35200.0 | 110000.0 | 2399000.0 |
| 4 | televisions | 1000.0 | 2239016.444 | 8.702166e+06 | 1000.0 | 450000.0 | 989900.0 | 1799900.0 | 200000000.0 |
# Para generar la variable descuento (recordando que las publicaciones con descuento son las que tienen valor en original price)
final_base['discount'] = (final_base['original_price'] / final_base['price']) - 1
final_base['discount'] = final_base['discount'].astype(float)
# Subconjunto de la base solo con las publicaciones con descuento
discounts_base = final_base[final_base['original_price'].notnull()]
discounts_base.shape
(286, 59)
discounts_base.groupby('sub_cat')['discount'].agg("mean").sort_values(ascending = False)
sub_cat televisions 0.481795 smartwatches 0.388795 cellphones 0.372799 audio 0.319209 screens 0.193325 Name: discount, dtype: float64
fig = px.histogram(discounts_base, x="discount", facet_col = "sub_cat")
fig.show()
De las consideradas, el descuento promedio más alto está en la categoría/subcategoría "Televisores", seguido de la categoría de "Smartwatches". Sin embargo, el que más productos con descuentos tiene es la categoría "Celulares y Smartphones" (16.6%), como se puede notar en el eje Y de los histogramas y como se verifica en la siguiente tabla:
final_base['discount_h'] = np.where(pd.notnull(final_base['discount']), 1,0)
percentage_base = final_base.groupby(['sub_cat'])['discount_h'].agg(["sum", "count"])
percentage_base['perc'] = (percentage_base['sum']/percentage_base['count'])*100
percentage_base
| sum | count | perc | |
|---|---|---|---|
| sub_cat | |||
| audio | 9 | 1000 | 0.9 |
| cellphones | 163 | 1000 | 16.3 |
| screens | 9 | 1000 | 0.9 |
| smartwatches | 24 | 1000 | 2.4 |
| televisions | 81 | 1000 | 8.1 |